import json
from enum import Enum
from json import JSONDecodeError

from marshmallow import Schema, fields, validates, ValidationError, post_load
from marshmallow.fields import List


class BaseSchema(Schema):
    pass


class PaginationSchema(Schema):
    """
    分页接口参数基类检查器.
    """
    page_no = fields.Integer(missing=1)
    page_size = fields.Integer(missing=30)

    @validates('page_size')
    def validate_page_size(self, value):
        if value < 1:
            raise ValidationError('page_size must great 0')
        if value > 5000:
            raise ValidationError('page_size must less 1000')


class ExportSchema(PaginationSchema):
    """
    导出功能接口
    """
    export = fields.Boolean()

    @post_load
    def post_load_func(self, data):
        if data.get('export'):
            del data['page_no']
            del data['page_size']
            if hasattr(data, 'order_by'):
                del data['order_by']
        return data


class CNDatetimeField(fields.DateTime):
    """
    中国地区常用的 2018-01-01 12:00:00 格式，区别于iso的 2018-01-01T12:00:00
    """
    DEFAULT_FORMAT = '%Y-%m-%d %H:%M:%S'


class LoadDumpOptions(Enum):
    value = 1
    name = 0


class EnumField(fields.Field):
    """
    枚举字段
    """
    VALUE = LoadDumpOptions.value
    NAME = LoadDumpOptions.name

    def __init__(self, enum, by_value=True, load_by=None, dump_by=None, *args, **kwargs):
        self.enum = enum
        self.by_value = by_value

        if load_by is None:
            load_by = LoadDumpOptions.value if by_value else LoadDumpOptions.name

        if load_by not in LoadDumpOptions:
            raise ValueError(
                'Invalid selection for load_by must be EnumField.VALUE or EnumField.NAME, got {}'.format(load_by)
            )

        if dump_by is None:
            dump_by = LoadDumpOptions.value if by_value else LoadDumpOptions.name

        if dump_by not in LoadDumpOptions:
            raise ValueError(
                'Invalid selection for load_by must be EnumField.VALUE or EnumField.NAME, got {}'.format(dump_by)
            )

        self.load_by = load_by
        self.dump_by = dump_by

        super(EnumField, self).__init__(*args, **kwargs)

    def _serialize(self, value, attr, obj, **kwargs):
        if value is None:
            return None
        elif self.dump_by == LoadDumpOptions.value:
            return value.value
        else:
            return value.name

    def _deserialize(self, value, attr, data, **kwargs):
        if value is None:
            return None
        elif self.load_by == LoadDumpOptions.value:
            return self._deserialize_by_value(value, attr, data)
        else:
            return self._deserialize_by_name(value, attr, data)

    def _deserialize_by_value(self, value, attr, data):
        try:
            value = self._get_enum_value_type()(value)
            return self.enum(value)
        except ValueError:
            self.fail('by_value', input=value, value=value)

    def _deserialize_by_name(self, value, attr, data):
        if not isinstance(value, str):
            self.fail('must_be_string', input=value, name=value)

        try:
            return getattr(self.enum, value)
        except AttributeError:
            self.fail('by_name', input=value, name=value)

    def _get_enum_value_type(self):
        return type(list(self.enum)[0].value)

    def fail(self, key, **kwargs):
        option_values = ', '.join([str(mem.value) for mem in self.enum])
        option_names = ', '.join([mem.name for mem in self.enum])

        if key == 'by_value':
            raise ValidationError('value: <{}> not in [{}]'.format(kwargs['value'], option_values))
        elif key == 'by_name':
            raise ValidationError('name: <{}> not in [{}]'.format(kwargs['name'], option_names))
        else:
            super().fail(key, **kwargs)


class OptionField(fields.Field):
    """
    从预置数据列表中选择一项
    """
    VALUE = LoadDumpOptions.value
    NAME = LoadDumpOptions.name

    def __init__(self, options, *args, **kwargs):
        assert isinstance(options, list)
        assert options, 'options must not empty'
        self.options = options
        self.value_type = type(options[0])
        for each in options:
            if self.value_type != type(each):
                assert 'each value in options must be same type'

        super().__init__(*args, **kwargs)

    def _deserialize(self, value, attr, data, **kwargs):
        try:
            value = self.value_type(value)
            if value not in self.options:
                raise ValidationError('value: <{}> is not in options: {}'.format(value, self.options))
            return value
        except ValueError:
            raise ValidationError('value: <{}> type is not {}'.format(value, self.value_type))


class GenericList(List):
    def _deserialize(self, value, attr, data, **kwargs):
        if isinstance(value, str):
            try:
                value = json.loads(value)
            except JSONDecodeError:
                raise ValidationError('value: <{}> type is not list type'.format(value))
        return super()._deserialize(value, attr, data, **kwargs)
